package commons;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import play.Play;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import commons.ImageMagickResizer.StreamGobbler;

/**
 * Reads metadata from a file using identify & Phil Harvey's exiftool
 * 
 */
public class Metadata {

    public final class NotAnImageException extends RuntimeException {
        private NotAnImageException(String s) {
            super(s);
        }

        private NotAnImageException(String s, Exception e) {
            super(s, e);
        }
    }

    static final Log logger = LogFactory.getLog(Metadata.class);
    static final Pattern IDENTIFY_PATTERN = Pattern.compile("^[A-Za-z0-9/\\._-]+ ([A-Z]+) ([0-9]+)x([0-9]+) .*");

    public String format;
    public int width;
    public int height;
    public long fileLength;
    public String identifyString;
    /**
     * Dumped by exiftool
     */
    HashMap<String, String> exifs = new HashMap<String, String>();



    /**
     * Load metadata of this image using identify & exiftool.
     * 
     * @param file
     * @throws NotAnImageException if imagemagick (identify) fails to recognize
     *             the image format (so having the image in the image server is
     *             pointless)
     */
    public Metadata(File file) throws NotAnImageException {
        fileLength = file.length();
        identify(file);
        readExifs(file);
    }

    private void identify(File file) throws NotAnImageException {
        // execute identify against provided file.
        try {
            Process p = Runtime.getRuntime().exec(new String[] {"identify", file.getAbsolutePath()});
            ByteArrayOutputStream stdout = new ByteArrayOutputStream();
            ByteArrayOutputStream stderr = new ByteArrayOutputStream();
            StreamGobbler out = new StreamGobbler(p.getInputStream(), stdout, "Identify " + file.getName() + " stdout Gobbler");
            out.start();
            StreamGobbler err = new StreamGobbler(p.getErrorStream(), stderr, "Identify " + file.getName() + " stderr Gobbler");
            err.start();
            int exitValue = p.waitFor();
            // worst case scenario, the processor is about to die, we still wait
            // for 20 seconds for the streams to be flushed and their
            // corresponding threads trashed...
            out.join(20000);
            err.join(20000);
            if (exitValue != 0) {
                throw new NotAnImageException("Identify returned an error, stderr: " + new String(stderr.toByteArray(), "UTF-8"));
            }
            // right exit value => parse stdin.
            identifyString = new String(stdout.toByteArray(), "UTF-8");
            Matcher m = IDENTIFY_PATTERN.matcher(identifyString.replace('\n', ' '));
            if (!m.matches()) {
                throw new NotAnImageException("Identify didn't returned a valid output string: " + identifyString);
            }
            format = m.group(1);
            try {
                width = Integer.parseInt(m.group(2));
                height = Integer.parseInt(m.group(3));
            } catch (NumberFormatException nfe) {
                throw new NotAnImageException("Identify didn't returned a valid output string (unparsable width or height): " + identifyString, nfe);
            }
        } catch (IOException ioe) {
            // this exception will be used at debug time
            throw new NotAnImageException("Unable to execute identify", ioe);
        } catch (InterruptedException ie) {
            throw new NotAnImageException("Interrupted while executing identify", ie);
        }
    }

    // this method should never fail
    private void readExifs(File file) {
        // execute identify against provided file.
        try {
            // exiftool is really nice as it outputs clean json :)
            Process p = Runtime.getRuntime().exec(
                    new String[] {Play.configuration.getProperty("images.script.path") + "/exiftool", "-j", file.getAbsolutePath()});
            ByteArrayOutputStream stdout = new ByteArrayOutputStream();
            ByteArrayOutputStream stderr = new ByteArrayOutputStream();
            StreamGobbler out = new StreamGobbler(p.getInputStream(), stdout, "Exiftool " + file.getName() + " stdout Gobbler");
            out.start();
            StreamGobbler err = new StreamGobbler(p.getErrorStream(), stderr, "Exiftool " + file.getName() + " stderr Gobbler");
            err.start();
            int exitValue = p.waitFor();
            // worst case scenario, the processor is about to die, we still wait
            // for 20 seconds for the streams to be flushed and their
            // corresponding threads trashed...
            out.join(20000);
            err.join(20000);
            if (exitValue != 0) {
                logger.warn("Exiftool returned an error, stderr: " + new String(stderr.toByteArray(), "UTF-8"));
                return;
            }
            String stdOutAsString = new String(stdout.toByteArray(), "UTF-8");
            stdOutAsString = stdOutAsString.substring(1, stdOutAsString.length() - 2);

            JsonParser parser = new JsonParser();

            JsonObject o = (JsonObject) parser.parse(stdOutAsString);
            for (Entry<String, JsonElement> entry : o.entrySet()) {
                exifs.put(entry.getKey(), entry.getValue().getAsString());
            }
        } catch (IOException ioe) {
            // this exception will be used at debug time
            logger.warn("Unable to execute exiftool", ioe);
        } catch (InterruptedException ie) {
            logger.warn("Interrupted while executing exiftool", ie);
        } catch (Exception e) {
            logger.warn("An error occured during exif retrieval", e);
        }

    }

    @Override
    public String toString() {
        return identifyString;
    }
}
